Skip to content

ci: share CloudFront cache policy + KV store across preview stages#859

Open
willwashburn wants to merge 3 commits into
mainfrom
cleanup/share-preview-infra
Open

ci: share CloudFront cache policy + KV store across preview stages#859
willwashburn wants to merge 3 commits into
mainfrom
cleanup/share-preview-infra

Conversation

@willwashburn
Copy link
Copy Markdown
Member

Summary

  • web/scripts/bootstrap-preview-infra.sh — one-shot, idempotent script that creates a shared CloudFront CachePolicy (matching SST's internal default config) and KeyValueStore, then writes their IDs to SSM (/relay-web/preview/cache-policy-id, /relay-web/preview/kv-store-arn). Safe to re-run; only creates if SSM-recorded resources don't exist.
  • web/sst.config.ts — non-prod stages now read those SSM params and pass them to sst.aws.Nextjs as cachePolicy and edge.viewerRequest.kvStore. Production keeps its own resources for isolation.

Why

Each sst.aws.Nextjs preview stage was creating its own WebServerCachePolicy and WebKvStore. Per-account CloudFront quotas — 20 cache policies, 5 KV stores — capped how many concurrent previews could exist, and Deploy Preview was failing with TooManyCachePolicies / EntityLimitExceeded once leaked stages accumulated.

After this change, preview stages contribute 0 cache policies and 0 KV stores. Only the one shared pair counts against the quota, regardless of how many preview PRs are open.

Safety: shared KV store is OK

SST's router code namespaces every key by md5($app.name + $app.stage + componentName):

const kvNamespace = crypto.createHash('md5')
  .update(`${$app.name}-${$app.stage}-${name}`)
  .digest('hex').substring(0, 4);
// keys are written as `${kvNamespace}:metadata`, etc.

So pr-839 and pr-778 reading/writing the same KV store can't collide.

Rollout

  1. Run web/scripts/bootstrap-preview-infra.sh once (with AWS_PROFILE=ar_preview or ambient creds) to provision the shared resources and populate SSM.
  2. Merge this PR.
  3. The first preview deploy after merge will pick up the shared resources via SSM. Existing stages get migrated on their next deploy (SST will detect the cachePolicy/kvStore change and delete the old resources).

Pairs with #858 (the stale-stage cleanup workflow) — together they remove the cap-and-leak problem entirely.

Test plan

  • web/scripts/bootstrap-preview-infra.sh — runs cleanly, SSM params populated, AWS CachePolicy + KVStore visible in console.
  • Re-run web/scripts/bootstrap-preview-infra.sh — idempotent, doesn't create duplicates.
  • Deploy a preview stage after merge — completes without TooManyCachePolicies/EntityLimitExceeded, no per-stage WebServerCachePolicy or WebKvStore resources appear in the SST output.
  • Preview site routing/caching works as expected (router metadata served, cached responses cached).
  • Production deploy unchanged — confirm prod still owns its own cache policy and KV store.

🤖 Generated with Claude Code

Each sst.aws.Nextjs preview stage created its own CloudFront CachePolicy
and KeyValueStore. The per-account quotas (20 cache policies / 5 KV
stores) capped concurrent previews and produced TooManyCachePolicies /
EntityLimitExceeded once leaked stages accumulated.

- web/scripts/bootstrap-preview-infra.sh: one-shot, idempotent script that
  creates a shared CachePolicy (matching SST's internal default config)
  and KV store, then publishes their IDs to SSM
  (/relay-web/preview/cache-policy-id, /relay-web/preview/kv-store-arn).
- web/sst.config.ts: non-prod stages now read those SSM params and pass
  them to sst.aws.Nextjs as `cachePolicy` and `edge.viewerRequest.kvStore`.
  Production keeps creating its own resources for isolation.

SST namespaces KV entries by md5(app + stage + componentName), so the
shared store is safe — keys from different preview stages don't collide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7081c89a-a54d-4ec0-8dc0-9b7fa028b50c

📥 Commits

Reviewing files that changed from the base of the PR and between cfa4b2c and 3ed2f6d.

📒 Files selected for processing (1)
  • web/scripts/bootstrap-preview-infra.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/scripts/bootstrap-preview-infra.sh

📝 Walkthrough

Walkthrough

Adds an idempotent Bash bootstrap script that provisions (or reuses) a shared CloudFront cache policy and key-value store and stores their identifiers in SSM. sst.config.ts now reads those SSM parameters for non-production stages and conditionally injects them into the Next.js deployment.

Changes

Preview Infrastructure with Shared CloudFront Resources

Layer / File(s) Summary
CloudFront resource bootstrap script
web/scripts/bootstrap-preview-infra.sh
New idempotent bash script validates AWS dependencies (aws, jq), reads/writes SSM parameters, verifies or creates a CloudFront cache policy and a CloudFront key-value store, and stores their IDs/ARNs back to SSM for reuse across preview stages.
SST configuration for preview stages
web/sst.config.ts
Reads preview CloudFront cache policy ID and KV store ARN from SSM for non-production stages and conditionally injects the cache policy and edge viewerRequest KV store configuration into the sst.aws.Nextjs deployment.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • AgentWorkforce/cloud#471: Related to provisioning and reusing a shared CloudFront cache policy and KV store for Next.js preview stacks.

Suggested reviewers

  • khaliqgant

Poem

🐇 I dug a tiny tunnel deep,
Into SSM where secrets sleep,
I bootstrapped caches, one for all,
So previews stand, and risers fall,
Hop, deploy — the edges leap! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: sharing CloudFront cache policy and KV store across preview stages, which is the primary objective of the PR.
Description check ✅ Passed The description is comprehensive and well-structured, covering summary, rationale, safety considerations, rollout steps, and test plan—far exceeding the template requirements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cleanup/share-preview-infra

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/scripts/bootstrap-preview-infra.sh`:
- Around line 35-37: The read_ssm function currently suppresses all aws ssm
errors; change it to only treat a missing parameter as non-fatal by calling aws
ssm get-parameter and inspecting its stderr/exit status: if the error message
contains "ParameterNotFound" return empty, otherwise propagate the failure (log
the error and exit non-zero or return a non-empty failure) so
AccessDenied/throttling/transport errors are not masked; update the read_ssm
implementation to capture stderr, check for the "ParameterNotFound" string and
handle only that case silently, while letting other errors surface.
- Around line 17-19: The script only checks AWS_ACCESS_KEY_ID before setting
AWS_PROFILE, which overrides valid OIDC/assumed-role credentials; change the
conditional that sets AWS_PROFILE to only run when none of the common credential
sources are present — e.g. check that AWS_ACCESS_KEY_ID,
AWS_WEB_IDENTITY_TOKEN_FILE, and AWS_ROLE_ARN (and optionally AWS_SESSION_TOKEN)
are all empty/unset before exporting AWS_PROFILE="${AWS_PROFILE:-ar_preview}" so
role-based or web-identity authentication is not overridden.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e6659cc-8551-4a9c-bb73-2ce303350b9c

📥 Commits

Reviewing files that changed from the base of the PR and between ae04694 and cfa4b2c.

📒 Files selected for processing (2)
  • web/scripts/bootstrap-preview-infra.sh
  • web/sst.config.ts

Comment thread web/scripts/bootstrap-preview-infra.sh Outdated
Comment thread web/scripts/bootstrap-preview-infra.sh
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread web/scripts/bootstrap-preview-infra.sh Outdated
# 2. KV store. SST namespaces keys by md5(app + stage + componentName) so sharing
# the store across stages is safe — entries don't collide.
existing_kv_arn="$(read_ssm "$SSM_KV_STORE_PARAM")"
if [ -n "$existing_kv_arn" ] && aws cloudfront describe-key-value-store --kvs-arn "$existing_kv_arn" >/dev/null 2>&1; then
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Wrong CLI flag --kvs-arn for describe-key-value-store breaks script idempotency

The AWS CLI command aws cloudfront describe-key-value-store accepts --name (which can be a name or an ARN), not --kvs-arn. The --kvs-arn flag is only valid for data-plane KVS operations like list-keys-in-key-value-store, get-key-in-key-value-store, etc. Because --kvs-arn is unrecognized, the command on line 85 always fails (silently, due to 2>&1), causing the if to evaluate false and the script to fall into the else branch. On a re-run where the KV store already exists, create-key-value-store will fail with an AlreadyExists conflict, and set -euo pipefail will abort the script. This directly breaks the script's stated idempotency guarantee.

Suggested change
if [ -n "$existing_kv_arn" ] && aws cloudfront describe-key-value-store --kvs-arn "$existing_kv_arn" >/dev/null 2>&1; then
if [ -n "$existing_kv_arn" ] && aws cloudfront describe-key-value-store --name "$existing_kv_arn" >/dev/null 2>&1; then
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cfa4b2cb1c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +46 to +47
existing_policy_id="$(read_ssm "$SSM_CACHE_POLICY_PARAM")"
if [ -n "$existing_policy_id" ] && aws cloudfront get-cache-policy --id "$existing_policy_id" >/dev/null 2>&1; then
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse existing cache policy when SSM parameter is missing

This idempotency check only trusts the SSM value, so if the first run creates the policy but fails before put-parameter (or if the SSM param is later deleted), a rerun will execute create-cache-policy with the same fixed name and fail. AWS CloudFront requires cache policy names to be unique (CachePolicyAlreadyExists), so the script cannot recover automatically from partial failures even though it is documented as safe to rerun.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/scripts/bootstrap-preview-infra.sh">

<violation number="1" location="web/scripts/bootstrap-preview-infra.sh:23">
P2: The script requires `jq` but never uses it. All JSON handling uses heredocs and AWS CLI's `--query`/`--output text`. Remove `jq` from the dependency check to avoid unnecessarily blocking execution.</violation>

<violation number="2" location="web/scripts/bootstrap-preview-infra.sh:36">
P1: `read_ssm()` suppresses all errors (`2>/dev/null || true`), not just `ParameterNotFound`. If the call fails due to AccessDenied, throttling, or network issues, the function returns an empty string and the script interprets it as "parameter missing" — then incorrectly attempts to create duplicate resources. Only `ParameterNotFound` should be treated as an empty result; other failures should abort the script.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.
Re-trigger cubic

Comment thread web/scripts/bootstrap-preview-infra.sh Outdated
Comment thread web/scripts/bootstrap-preview-infra.sh Outdated
@willwashburn
Copy link
Copy Markdown
Member Author

@copilot fix issues raised by comments in the PR thread

Copy link
Copy Markdown
Contributor

Copilot AI commented May 18, 2026

@copilot fix issues raised by comments in the PR thread

All issues from the review comments have been addressed in commit 3ed2f6d:

  1. Credential check — now checks AWS_PROFILE, AWS_ACCESS_KEY_ID, AWS_WEB_IDENTITY_TOKEN_FILE, and AWS_ROLE_ARN; only falls back to ar_preview when none are set.
  2. read_ssm error handling — re-written to only treat ParameterNotFound as a non-fatal empty result; AccessDenied/throttling/network errors now propagate and abort the script.
  3. Unused jq dependency — removed from the dependency check.
  4. Cache policy idempotency — when SSM is missing or stale, now falls back to list-cache-policies --type custom lookup by name before creating, preventing CachePolicyAlreadyExists on re-runs after partial failures.
  5. KV store --kvs-arn bug — changed to --name (which accepts ARNs as well as names per the AWS CLI docs), and added the same lookup-by-name fallback for full idempotency.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/scripts/bootstrap-preview-infra.sh">

<violation number="1" location="web/scripts/bootstrap-preview-infra.sh:110">
P2: The KV-store existence check passes an ARN into `--name`, so the check never succeeds. This bypasses the intended SSM fast-path and can cause unnecessary fallback calls/failures.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Fix all with cubic | Re-trigger cubic

# 2. KV store. SST namespaces keys by md5(app + stage + componentName) so sharing
# the store across stages is safe — entries don't collide.
existing_kv_arn="$(read_ssm "$SSM_KV_STORE_PARAM")"
if [ -n "$existing_kv_arn" ] && aws cloudfront describe-key-value-store --name "$existing_kv_arn" >/dev/null 2>&1; then
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The KV-store existence check passes an ARN into --name, so the check never succeeds. This bypasses the intended SSM fast-path and can cause unnecessary fallback calls/failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web/scripts/bootstrap-preview-infra.sh, line 110:

<comment>The KV-store existence check passes an ARN into `--name`, so the check never succeeds. This bypasses the intended SSM fast-path and can cause unnecessary fallback calls/failures.</comment>

<file context>
@@ -72,27 +96,37 @@ else
 #    the store across stages is safe — entries don't collide.
 existing_kv_arn="$(read_ssm "$SSM_KV_STORE_PARAM")"
-if [ -n "$existing_kv_arn" ] && aws cloudfront describe-key-value-store --kvs-arn "$existing_kv_arn" >/dev/null 2>&1; then
+if [ -n "$existing_kv_arn" ] && aws cloudfront describe-key-value-store --name "$existing_kv_arn" >/dev/null 2>&1; then
   echo "==> KV store already exists: $existing_kv_arn"
   kv_store_arn="$existing_kv_arn"
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants